package net.towee.client.managers.impl;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import net.towee.client.event.core.DocumentReceivedEvent;
import net.towee.client.event.core.DocumentReceivedHandler;
import net.towee.client.event.core.NodeReceivedEvent;
import net.towee.client.event.core.NodesMovedEvent;
import net.towee.client.event.core.NodesRemovedEvent;
import net.towee.client.event.core.NodesTrashEvent;
import net.towee.client.managers.AccountHelper;
import net.towee.client.managers.NodesManager;
import net.towee.model.account.AccountPermission;
import net.towee.model.repo.DocumentData;
import net.towee.model.repo.NodeData;
import net.towee.model.repo.impl.NodeDataImpl;
import net.towee.model.utils.Pair;
import net.towee.server.rpc.services.ToweeServiceAsync;

import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.gwt.event.shared.HandlerManager;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.inject.Inject;

public class NodesManagerImpl implements NodesManager {
	private final ToweeServiceAsync service;
	private final HandlerManager eventBus;
	private final AccountHelper account;

	private final Map<String, List<NodeData>> nodesParentCache;
	private final Map<NodeData, String> nodesToParent;
	private final Map<String, NodeData> nodesCache;

	@Inject
	public NodesManagerImpl(ToweeServiceAsync service, HandlerManager eventBus, AccountHelper account) {
		this.service = service;
		this.eventBus = eventBus;
		this.account = account;

		// this.nodesCache = new MapMaker().maximumSize(100).makeMap();
		// this.nodesToParent = new MapMaker().maximumSize(500).makeMap();
		this.nodesParentCache = new HashMap<String, List<NodeData>>();
		this.nodesToParent = new HashMap<NodeData, String>();
		this.nodesCache = new HashMap<String, NodeData>();

		createPushHandler();
	}

	private void createPushHandler() {
		eventBus.addHandler(DocumentReceivedEvent.TYPE, new DocumentReceivedHandler() {
			@Override
			public void onDocumentReceived(DocumentData result, boolean local) {
				// Put to cache document node
				putToCache(result.getNode());

				// Fire an event
				eventBus.fireEvent(new NodeReceivedEvent(result.getNode()));
			}
		});

		service.onNodesChange(null, null, new AsyncCallback<List<NodeData>>() {
			@Override
			public void onFailure(Throwable caught) {
			}

			@Override
			public void onSuccess(List<NodeData> result) {
				// Update cache
				for (NodeData node : result) {
					// Remove old node
					String parentNode = removeFromCache(node);

					// Add new node
					if (parentNode != null)
						nodesParentCache.get(parentNode).add(node);
				}

				// Fire an event
				eventBus.fireEvent(new NodeReceivedEvent(result));
			}
		});

		service.onNodesRemoved(null, null, new AsyncCallback<List<NodeData>>() {
			@Override
			public void onFailure(Throwable caught) {
			}

			@Override
			public void onSuccess(List<NodeData> result) {
				// Update cache
				for (NodeData node : result) {
					removeFromCache(node);
				}

				// Fire an event
				eventBus.fireEvent(new NodesRemovedEvent(result));
			}
		});

		service.onNodesToTrash(null, null, new AsyncCallback<List<NodeData>>() {
			@Override
			public void onFailure(Throwable caught) {
			}

			@Override
			public void onSuccess(List<NodeData> result) {
				// Update cache
				for (NodeData node : result) {
					removeFromCache(node);
				}

				// Fire an event
				eventBus.fireEvent(new NodesTrashEvent(result));
			}

		});

		service.onNodesMoved(null, null, new AsyncCallback<Pair<String, List<NodeData>>>() {
			@Override
			public void onFailure(Throwable caught) {
			}

			@Override
			public void onSuccess(Pair<String, List<NodeData>> result) {
				// Remove
				for (NodeData node : result.getSecond()) {
					// Remove from old place
					removeFromCache(node);

					// Get new parentNode id
					String parentNode = node.getParentId() != null ? node.getParentId() : node
							.getGroupId();

					// Add to new place if this place exists
					if (nodesParentCache.containsKey(parentNode)) {
						nodesParentCache.get(parentNode).add(node);
						nodesToParent.put(node, parentNode);
					}
				}

				// Fire an event
				eventBus.fireEvent(new NodesMovedEvent(result.getFirst(), result.getSecond()));
			}

		});
	}

	/**
	 * Remove given node from cache if it exists
	 * 
	 * @param node
	 * @return
	 */
	private String removeFromCache(NodeData node) {
		// Get parent id
		String parentNode = nodesToParent.get(node);

		if (parentNode != null) {
			nodesParentCache.get(parentNode).remove(node);
			nodesToParent.remove(node);
			nodesCache.remove(node.getId());
		}

		return parentNode;
	}

	private void putToCache(String parentNode, List<NodeData> nodes) {
		nodesParentCache.put(parentNode, nodes);

		for (NodeData node : nodes) {
			nodesToParent.put(node, parentNode);
			nodesCache.put(node.getId(), node);
		}
	}

	private void putToCache(NodeData node) {
		String parentId = node.getParentId() != null ? node.getParentId() : node.getGroupId();

		nodesCache.put(node.getId(), node);
		nodesToParent.put(node, parentId);

		if (nodesParentCache.containsKey(parentId)) {
			nodesParentCache.get(parentId).remove(node);
			nodesParentCache.get(parentId).add(node);
		}
	}

	@Override
	public void getRootNodes(final String groupId, final AsyncCallback<List<NodeData>> handler) {
		Preconditions.checkArgument(!Strings.isNullOrEmpty(groupId), "null group id");

		if (nodesParentCache.containsKey(groupId))
			handler.onSuccess(nodesParentCache.get(groupId));
		else
			service.getRootNodes(groupId, new AsyncCallback<List<NodeData>>() {
				@Override
				public void onFailure(Throwable caught) {
					// TODO
				}

				@Override
				public void onSuccess(List<NodeData> result) {
					putToCache(groupId, result);
					handler.onSuccess(result);
				}
			});
	}

	@Override
	public void getSubTree(String groupId, final String parentNodeId,
			final AsyncCallback<List<NodeData>> handler) {
		Preconditions.checkArgument(!Strings.isNullOrEmpty(parentNodeId), "null parent node id");
		Preconditions.checkArgument(!Strings.isNullOrEmpty(groupId), "null group id");

		if (nodesParentCache.containsKey(parentNodeId))
			handler.onSuccess(nodesParentCache.get(parentNodeId));
		else
			service.getSubTree(groupId, parentNodeId, new AsyncCallback<List<NodeData>>() {
				@Override
				public void onFailure(Throwable caught) {

				}

				@Override
				public void onSuccess(List<NodeData> result) {
					putToCache(parentNodeId, result);
					handler.onSuccess(result);
				}
			});
	}

	@Override
	public void getSubTreeByPath(String groupId, String path, final AsyncCallback<List<NodeData>> handler) {
		service.getSubTreeByPath(groupId, path, new AsyncCallback<List<NodeData>>() {
			@Override
			public void onFailure(Throwable caught) {

			}

			@Override
			public void onSuccess(List<NodeData> result) {
				handler.onSuccess(result);
			}
		});
	}

	@Override
	public void getSubTreesToPath(String groupId, String path, final AsyncCallback<List<NodeData>> handler) {
		service.getSubTreeToPath(groupId, path, new AsyncCallback<List<NodeData>>() {
			@Override
			public void onFailure(Throwable caught) {

			}

			@Override
			public void onSuccess(List<NodeData> result) {
				handler.onSuccess(result);
			}
		});
	}

	@Override
	public void getNode(String nodeId, String groupId, final AsyncCallback<NodeData> handler) {
		if (nodeId == null)
			handler.onSuccess(new NodeDataImpl(null, groupId, account.getAccountGroup(groupId)
					.getGroupIdentity().getPrintName()));
		else if (nodesCache.containsKey(nodeId))
			handler.onSuccess(nodesCache.get(nodeId));
		else
			service.getNode(nodeId, new AsyncCallback<NodeData>() {
				@Override
				public void onFailure(Throwable caught) {
					// TODO
				}

				@Override
				public void onSuccess(NodeData result) {
					putToCache(result);
					handler.onSuccess(result);
				}
			});
	}

	@Override
	public void saveNode(NodeData node, final AsyncCallback<NodeData> handler) {
		Preconditions.checkArgument(!Strings.isNullOrEmpty(node.getName()), "null node name");
		Preconditions.checkArgument(!Strings.isNullOrEmpty(node.getGroupId()), "null group id");
		Preconditions.checkArgument(
				account.hasPermissions(node.getGroupId(), AccountPermission.CAN_ADD_DOCUMENT),
				"no permission");

		service.saveNode(node, new AsyncCallback<NodeData>() {
			@Override
			public void onFailure(Throwable caught) {
				handler.onFailure(caught);
			}

			@Override
			public void onSuccess(NodeData result) {
				// Put to cache
				putToCache(result);

				// Invoke handler
				handler.onSuccess(result);

				// Fire an event
				eventBus.fireEvent(new NodeReceivedEvent(result));
			}
		});
	}

	@Override
	public void renameNode(String groupId, final String nodeId, final String newName,
			final AsyncCallback<Boolean> handler) {
		Preconditions.checkArgument(!Strings.isNullOrEmpty(nodeId), "null node id");
		Preconditions.checkArgument(!Strings.isNullOrEmpty(newName), "null node name");
		Preconditions.checkNotNull(groupId, "null groupId");
		Preconditions.checkArgument(account.hasPermissions(groupId, AccountPermission.CAN_RENAME_NODES),
				"no permission");

		service.renameNode(groupId, nodeId, newName, new AsyncCallback<Boolean>() {
			@Override
			public void onFailure(Throwable caught) {
				handler.onFailure(caught);
			}

			@Override
			public void onSuccess(Boolean result) {
				Preconditions.checkArgument(result, "fail rename node");

				// Update cache
				NodeData node = nodesCache.get(nodeId);
				node.setName(newName);

				// Invoke handler
				handler.onSuccess(result);

				// Fire an event
				eventBus.fireEvent(new NodeReceivedEvent(node));
			}

		});
	}

	@Override
	public void moveNodes(String groupId, final String newParentNode, final List<String> nodeIds,
			final AsyncCallback<Boolean> handler) {
		// Check permissions and data
		Preconditions.checkNotNull(groupId, "null group id");
		Preconditions.checkNotNull(nodeIds, "null nodes");
		Preconditions.checkNotNull(newParentNode, "null new parent node");
		Preconditions.checkArgument(account.hasPermissions(groupId, AccountPermission.CAN_MOVE_NODES),
				"no permission");

		service.moveNodes(groupId, newParentNode, nodeIds, new AsyncCallback<Boolean>() {
			@Override
			public void onFailure(Throwable caught) {
				handler.onFailure(caught);
			}

			@Override
			public void onSuccess(Boolean result) {
				// TODO
			}
		});
	}

	@Override
	public void removeNode(final String groupId, final List<String> nodeIds, final AsyncCallback<Boolean> handler) {
		// Check permissions and data
		Preconditions.checkNotNull(groupId, "null group id");
		Preconditions.checkNotNull(nodeIds, "null nodes");
		Preconditions.checkArgument(account.hasPermissions(groupId, AccountPermission.CAN_REMOVE_NODE),
				"no permission");

		service.removeNode(groupId, nodeIds, new AsyncCallback<Boolean>() {

			@Override
			public void onFailure(Throwable caught) {
				handler.onFailure(caught);
			}

			@Override
			public void onSuccess(Boolean result) {
				List<NodeData> removedNodes = new ArrayList<NodeData>();

				// Remove nodes from cache
				for (String id : nodeIds) {
					NodeData node = nodesCache.get(id);
					removedNodes.add(node);
					removeFromCache(node);
				}

				// Fire an event
				eventBus.fireEvent(new NodesRemovedEvent(removedNodes));
			}
		});
	}

	@Override
	public void putToTrash(String groupId, final List<String> nodeIds,
			final AsyncCallback<Boolean> handler) {
		// Check permissions and data
		Preconditions.checkNotNull(groupId, "null group id");
		Preconditions.checkNotNull(nodeIds, "null nodes");
		Preconditions.checkArgument(account.hasPermissions(groupId, AccountPermission.CAN_PUT_TO_TRASH),
				"no permission");

		// Do put to trash
		service.putToTrash(groupId, nodeIds, new AsyncCallback<Boolean>() {
			@Override
			public void onFailure(Throwable caught) {
				handler.onFailure(caught);
			}

			@Override
			public void onSuccess(Boolean result) {
				List<NodeData> removedNodes = new ArrayList<NodeData>();

				// Remove nodes from cache
				for (String id : nodeIds) {
					NodeData node = nodesCache.get(id);
					removedNodes.add(node);
					removeFromCache(node);
				}

				// Fire an event
				eventBus.fireEvent(new NodesTrashEvent(removedNodes));
			}
		});
	}

	@Override
	public void checkNodeExisting(String nodeName, String groupId, String parentNode,
			AsyncCallback<Boolean> asyncCallback) {
		Preconditions.checkArgument(!Strings.isNullOrEmpty(nodeName), "null node name");
		Preconditions.checkArgument(!Strings.isNullOrEmpty(groupId), "null group id");

		List<NodeData> nodes = nodesParentCache.get(parentNode != null ? parentNode : groupId);

		if (nodes == null)
			service.checkNodeExisting(nodeName, groupId, parentNode, asyncCallback);
		else {
			for (NodeData node : nodes) {
				if (node.getName().equals(nodeName)) {
					asyncCallback.onSuccess(true);
					return;
				}
			}

			asyncCallback.onSuccess(false);
		}
	}

	@Override
	public void getPathToNode(NodeData node, AsyncCallback<LinkedList<NodeData>> asyncCallback) {
		Preconditions.checkNotNull(node, "null node");

		// Create result list
		LinkedList<NodeData> result = new LinkedList<NodeData>();
		result.add(node);

		// Get parent nodes
		doGetPathToNode(node, result, asyncCallback);
	}

	private void doGetPathToNode(NodeData node, final LinkedList<NodeData> nodes,
			final AsyncCallback<LinkedList<NodeData>> asyncCallback) {
		if (node.getParentId() == null) {
			asyncCallback.onSuccess(nodes);
		} else {
			getNode(node.getParentId(), node.getGroupId(), new AsyncCallback<NodeData>() {
				@Override
				public void onFailure(Throwable caught) {
				}

				@Override
				public void onSuccess(NodeData result) {
					nodes.addFirst(result);
					doGetPathToNode(result, nodes, asyncCallback);
				}
			});
		}
	}
}
